#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <float.h>

#include "pub_lib.h"
#include "shader.h"

/* global vars */
static Vector3 ka = {0.005, 0.005, 0.005};
static Vector3 ks = {0.7937, 0.7937, 0.7937};

static float kh = 0.2f;
static float kn = 0.1f;

static struct light L1 = {{20, 20, 20}, {500, 500, 500}};
static struct light L2 = {{-20, 20, 0}, {500, 500, 500}};

static Vector3 amb_light_intensity = {10, 10, 10};
static Vector3 eye_pos = {0, 0, 10};

/* Graphics Pipeline: Vertex Processing */

/*
 * Model, View, projection: MVP
 * MVP transformation: Vertex processing
 * or called vertex_shader
 * */
Matrix get_view_matrix(Quaternion pos)
{
	Matrix result = { 1.0f, 0.0f, 0.0f, -pos.x,
		0.0f, 1.0f, 0.0f, -pos.y,
		0.0f, 0.0f, 1.0f, -pos.z,
		0.0f, 0.0f, 0.0f, 1.0f };

	return result;
}

/*
  cos(radian), -sin(radian), 0, 0,
  sin(radian), cos(radian), 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1;

MatrixRotate((Vector3){ 0.0f, 0.0f, 1.0f }, rotation_angle * DEG2RAD);
Rotation around z only
 * */
Matrix get_model_matrix(float rotation_angle)
{
	float cosv, sinv;
	float angle;

	angle = rotation_angle * DEG2RAD;
	cosv = cosf(angle);
	sinv = sinf(angle);

	Matrix rotation = {
		cosv, 0.0f, sinv, 0.0f,
		0.0f, 1.0f, 0.0f, 0.0f,
	   -sinv, 0.0f, cosv, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f };

	Matrix scale = {
		2.5, 0, 0, 0,
		0, 2.5, 0, 0,
		0, 0, 2.5, 0,
		0, 0, 0, 1 };

	Matrix result = matrxiMul(rotation, scale);
	return result;
}

/*
 * MatrixPerspective(eye_fov * DEG2RAD, aspect_ratio, zNear, zFar);
 ensure zNear and zFar is negative value
 * */
Matrix get_projection_matrix(float eye_fov, float aspect_ratio,
                        float zNear, float zFar)
{
	float val, top, bottom, left, right;
	float rl, tb, fn;
	Matrix result = {0};

	val = tanf(eye_fov * 0.5f * DEG2RAD);
	top = fabsf(zNear) * val;
	bottom = -top;
	right = aspect_ratio * top;
	left = -right;

	rl = right - left;
    tb = top - bottom;
    fn = zFar - zNear;

	result.m0 = zNear / right;
    result.m1 = 0.0f;
    result.m2 = 0.0f;
    result.m3 = 0.0f;

    result.m4 = 0.0f;
    result.m5 = zNear / top;
    result.m6 = 0.0f;
    result.m7 = 0.0f;

    result.m8 = 0.0f;
    result.m9 = 0.0f;
    result.m10 = -(zFar + zNear)/fn;
    result.m11 = -1.0f;

    result.m12 = 0.0f;
    result.m13 = 0.0f;
    result.m14 = -(zFar*zNear*2.0f)/fn;
    result.m15 = 0.0f;

	return result;
}

/*
	Graphics Pipeline: Fragment Processing
*/

/* u, v: [0, 1]
 * */
Vector3 texture_getColor(Image* pImage, float u, float v)
{
	if(u < 0.0f) 
		u = 0.0f;
	if(v < 0.0f) 
		v = 0.0f;
	if(u > 1.0f) 
		u = 1.0f;
	if(v > 1.0f) 
		v = 1.0f;

	float x = u * (pImage->width - 1);
	float y = v * (pImage->height  - 1); //CV diff with raylib
	Color c = GetImageColor(*pImage, (int)x, (int)y);
	Vector3 result = {c.r, c.g, c.b};
	return result;
}

/* x,y is (u, v) coordinate at texture image */
Vector3 getImgColor(Image* pImage, float x, float y)
{
	Color c = GetImageColor(*pImage, (int)x, (int)y);
	Vector3 result = {c.r, c.g, c.b};
	return result;
}

//Bilinear Interpolation
//https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/interpolation/bilinear-filtering
Vector3 getColorBilinear(Image* pImage, float u, float v)
{
	if(u < 0.0f)
		u = 0.0f;
	if(v < 0.0f)
		v = 0.0f;
	if(u > 1.0f)
		u = 1.0f;
	if(v > 1.0f)
		v = 1.0f;

	float u_img = u * (pImage->width - 1);
	float v_img = v * (pImage->height - 1);

	float u_min = floorf(u_img);
	float u_max = ceilf(u_img); //max - min : [0,1]
	float v_min = floorf(v_img);
	float v_max = ceilf(v_img); //max - min : [0,1]

	// C01 C11
	// C00 C10
	Vector3 c00 = getImgColor(pImage, u_min, v_min);
	Vector3 c10 = getImgColor(pImage, u_max, v_min);
	Vector3 c01 = getImgColor(pImage, u_min, v_max);
	Vector3 c11 = getImgColor(pImage, u_max, v_max);

	float s = u_img - u_min;
	float t = v_img - v_min;
	/*
	 * lerp(v1, v2, s) = v1 + s * (v2 - v1)
	 * lerp(v1, v2, s) = v1 * (1 - s) + v2 * s = v1 + s * (V2 - V1)
	 * lerp(left, right, s), lerp(bottom, top, t)
	 *
	 * a = 00 * (1 - s) + 10 * s
	 * b = 01 * (1 - s) + 11 * s
	 * c  = a * (1 - t) + b * t
	 * */
	Vector3 a = Vector3Lerp(c00, c10, s);
	Vector3 b = Vector3Lerp(c01, c11, s);
	Vector3 c = Vector3Lerp(a, b, t);

	return c;
}

Color normal_fragment_shader(struct fragment_shader_payload* payload)
{
	Vector3 normal = payload->normal;
	Vector3 avg = {1.0f, 1.0f, 1.0f};

	//(payload.normal.head<3>().normalized() + Eigen::Vector3f(1.0f, 1.0f, 1.0f)) / 2.f;
	normal = Vector3Normalize(normal);
	avg = Vector3Add(normal, avg);
	avg = Vector3Scale(avg, 0.5);
	Vector3 result_color = Vector3Scale(avg, 255);

    return  makeRGBA(result_color);
}

/*Blinn-Phong Reflectance Model 
 * input:
 * Light direction: light (default is 2 sets light)
 * Viewer direction: viewer
 * Suface Normal: n
 * Surface parameters: color
 *
 * output: diffuse + Specular + Ambient
 * */
Vector3 cal_light(struct light *light, Vector3 viewer, Vector3 n, Vector3 color)
{
	Vector3 kd = color;
    Vector3 point = viewer;
    Vector3 normal = n;

    Vector3 result_color = {0, 0, 0};
	Vector3 L, e, h;
	Vector3 diffuse, specular, ambient, temp;
	float r_len;
	for (int i = 0; i < 2; i++, light++)
    {
        // TODO: For each light source in the code
		// calculate what the *ambient*, *diffuse*, and *specular*
        // components are. Then, accumulate that result on the *result_color* object
		L = Vector3Subtract(light->position, point);//view_position -> light source
		r_len = Vector3LengthSqr(L);
		L = Vector3Normalize(L);

		e = Vector3Subtract(eye_pos, point);
		e = Vector3Normalize(e);

		h = Vector3Add(L, e);
		h = Vector3Normalize(h);

		//diffuse = kd.cwiseProduct(light.intensity / r_len) * std::max(0.f, l.dot(normal))
		//kd.cwiseProduct(light.intensity / r_len))
		Vector3 IScale = Vector3Scale(light->intensity, 1.0f/r_len);
		temp = Vector3Multiply(kd, IScale); //kd.cwiseProduct(temp)
		//std::max(0.f, l.dot(normal))
		float LdotN = Vector3DotProduct(L, normal);
		if (LdotN < 0.0f)
			LdotN = 0.0f;
		diffuse = Vector3Scale(temp, LdotN);//kd*I/(r*r) * max(0, L dot N)

       //specular = ks.cwiseProduct(light.intensity / pow(r, 2)) * pow(std::max(0.f, h.dot(normal)), p);
		temp = Vector3Multiply(ks, IScale);
		float HdotN = Vector3DotProduct(h, normal);
		if (HdotN < 0.0f) {
			HdotN = 0.0f;
		} else {
			HdotN = powf(HdotN, 150);
		}
		specular = Vector3Scale(temp, HdotN);

        //ambient = ka.cwiseProduct(amb_light_intensity);
		ambient = Vector3Multiply(ka, amb_light_intensity);
        //result_color += diffuse + specular + ambient;
		result_color = Vector3Add(result_color, diffuse);
		result_color = Vector3Add(result_color, specular);
		result_color = Vector3Add(result_color, ambient);
    }

	return result_color;
}

Color light2Color(Vector3 light_color)
{
	Color ret;
	if(light_color.x>=1.0 || light_color.y>=1.0 || light_color.z>=1.0)
	{
		ret = WHITE;
	} else {
		light_color = Vector3Scale(light_color, 255);
		ret = makeRGBA(light_color);
	}
	return ret;
}

Color phong_fragment_shader(struct fragment_shader_payload* payload)
{
	struct light light_ary[2] = {L1, L2};
	struct light *light = light_ary;

	Vector3 result = cal_light(light, payload->view_pos, payload->normal, payload->color);
	return light2Color(result);
}

Color texture_fragment_shader(struct fragment_shader_payload* payload)
{
	struct light light_ary[2] = {L1, L2};
	struct light *light = light_ary;

	//TODO: Get the texture value at the texture coordinates of the current fragment
	Vector3 texture_color = texture_getColor(payload->pImage,
		payload->tex_coords.x, payload->tex_coords.y);
	texture_color = Vector3Scale(texture_color, 1.0f/255.0f);

	Vector3 result = cal_light(light, payload->view_pos, payload->normal, texture_color);
	return light2Color(result);
}

/* calculate the normal by TBN matrix transform */
Vector3 cal_tbn_normal(struct fragment_shader_payload* payload)
{
	// TODO: Implement bump mapping here
    // Let n = normal = (x, y, z)
    // Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
    // Vector b = n cross product t
    // Matrix TBN = [t b n]
    // dU = kh * kn * (h(u+1/w,v)-h(u,v))
    // dV = kh * kn * (h(u,v+1/h)-h(u,v))
    // Vector ln = (-dU, -dV, 1)
    // Normal n = normalize(TBN * ln)

	Vector3 normal = payload->normal;
	float x = normal.x;
	float y = normal.y;
	float z = normal.z;
	// Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
	float a = sqrtf(x * x + z * z);
	Vector3 t = {(x * y / a), a, (z * y / a)};
	// Vector b = n cross product t
	Vector3 b = Vector3CrossProduct(normal, t);
	// Matrix TBN = [t b n]
	Matrix TBN = {
		t.x, b.x, normal.x, 0,
		t.y, b.y, normal.y, 0,
		t.z, b.z, normal.z, 0,
		0,	 0,			0,	1
	};

	float h = payload->pImage->height;
	float w = payload->pImage->width;
	float u = payload->tex_coords.x;
    float v	= payload->tex_coords.y;

	//du = kh * kn * (getColor(u + 1. / w, v).norm() - getColor(u, v).norm());
	Vector3 temp;
	temp = getColorBilinear(payload->pImage, u + 1.0f/w, v);
	x = Vector3Length(temp);
	temp = getColorBilinear(payload->pImage, u, v);
	y = Vector3Length(temp);
	float du = kh * kn * (x - y);

	//dv = kh * kn * (getColor(u, v + 1. / h).norm() - getColor(u, v).norm());
	temp = getColorBilinear(payload->pImage, u, v + 1.0f/h);
	x = Vector3Length(temp);
	float dv = kh * kn * (x - y);

	// Vector ln = (-dU, -dV, 1)
	Quaternion ln = {-du, -dv, 1, 1};
	//Normal n = normalize(TBN * ln)
	ln = QuaternionTransform(ln,  TBN);
	Vector3 n = {ln.x, ln.y, ln.z};
	normal = Vector3Normalize(n);

	return normal;
}

Color bump_fragment_shader(struct fragment_shader_payload* payload)
{
	Vector3 normal  = cal_tbn_normal(payload);
	//[-1,1] to [0,1]
	Vector3 full = {1.0, 1.0, 1.0};
	normal = Vector3Add(normal, full);
	normal = Vector3Scale(normal, 0.5f);

	Vector3 result_color = Vector3Scale(normal, 255);
	Color ret = makeRGBA(result_color);
	return ret;
}

//displacement
Color disp_fragment_shader(struct fragment_shader_payload* payload)
{
	// Position p = p + kn * n * h(u,v)
	//temp = h(u,v)
	Vector3 temp = getColorBilinear(payload->pImage,
			payload->tex_coords.x, payload->tex_coords.y);
	float val = kn * Vector3Length(temp);//val = kn * h(u,v)

	Vector3 normal  = cal_tbn_normal(payload); //TBN normal transform
	Vector3 padd = Vector3Scale(normal, val);//n * kn * h(u,v)

	Vector3 p = payload->view_pos;
	p = Vector3Add(p, padd); //p is view position

	struct light light_ary[2] = {L1, L2};
	struct light *light = light_ary;

	Vector3 result = cal_light(light, p, normal, payload->color);

	return light2Color(result);
}

